var FMFPdxfMapper = Class.create();
FMFPdxfMapper.prototype = {
    initialize: function() {
        this.facilities = {
            building: '',
            floor: ''
        };
    },
    
    /**
    * Function used to call the creation of Buildings and Floors
    * NEEDS TO CHANGE
    *
    * @param {String} versionSysId
    * @param {Boolean} isNewFloor
    * @param {String} building
    * @param {String} floor
    * @returns {void}
    */
    generateFacilities: function(versionSysId, isNewFloor, building, floor) {
        var facilityUtils = new FMFPFacilityUtils();
        this.facilities = facilityUtils.generateFacilities(versionSysId, isNewFloor, building, floor);
    },
    
    /**
    * Run and apply the mapping operations to the entity
    *
    * @param {String} lStdConfigSysId
    * @param {Object} entity
    * @returns {Object}
    */
    applyMappings: function(lStdConfigSysId, dataOperation, entity) {
        var recordData = {
            'table': '',
            'data': {},
            'coalesceFields': {}
        };

        var lStdObjMapRec = new GlideRecord(FMConstants.Table.LAYER_OBJECT_MAPPING);
        lStdObjMapRec.addQuery('layer_standard_configuration', lStdConfigSysId);
        lStdObjMapRec.query();
        while(lStdObjMapRec.next()) {
            var fieldValue = lStdObjMapRec.getDisplayValue('use_transform_script') == 'true' ?
                this._runTransformScript(lStdObjMapRec, entity) :
                this._readEntity(entity, lStdObjMapRec.getValue('object_property'));
            if(fieldValue) {
                var fieldName = lStdObjMapRec.getValue('servicenow_field');

                recordData.table = lStdObjMapRec.layer_standard_configuration.objects_table + '';

                if(lStdObjMapRec.getDisplayValue('coalesce') === 'true' &&
                    (this._isInsert(dataOperation) || this._isUpdate(dataOperation))) {
                    recordData.coalesceFields[fieldName] = fieldValue;
                }
                
                if(this._isInsert(dataOperation) ||
                    (this._isUpdate(dataOperation) && lStdObjMapRec.getDisplayValue('coalesce') !== 'true')) {
                    recordData.data[fieldName] = fieldValue;
                }
            }
        }

        if(Object.keys(recordData.data).length === 0) {
            return null;
        }

        // if the data operation is update but there is no field to coalesce, then do nothing
        if(this._isUpdate(dataOperation) && recordData.coalesceFields.length < 0) return null;

        this._actionOnRecord(recordData, dataOperation);
        
        return {
            coalesceFields: recordData.coalesceFields,
            table: recordData.table
        };
    },

    /**
    * Evaluate a JSON query
    *
    * Example: from obj {hello: 'world'} and query 'hello=="world"'
    * will return true because there's an 'hello' key whose value is 'world'.
    *
    * The value operand accepts strings, numbers and boolean true or false.
    *
    * The query also accepts && so that more than one condition can be used.
    *
    * Example: evaluate({hello: 'world', people: 3}, 'hello=="world" && people')
    * will return true because of hello key whose value is 'world' and
    * there's a 'people' key, no matter what value it has (unless it's undefined).
    *
    * @param {Obj} json
    * @param {String} query
    * @returns {Boolean} True if query evaluated successfully
    */
    evaluateCondition: function(json, query) {
        if (typeof json !== 'object')
            return false;
        if (!query || (typeof query === 'string' && query.trim() == ''))
            return true;

        var strippedQuery = this._stripWhitespace(query);

        // separate in case of AND operator to process each block
        var ands = strippedQuery.split(/&&/).map(function(a){return a.trim();});

        var lastResult;
        var typeCheck = false;

        for (var i in ands) {
            typeCheck = ands[i].includes('typeof ');
            if(typeCheck) {
                ands[i] = ands[i].replace('typeof ', '');
            }
            var split = ands[i]
                .match(/\s*(['"][^'"]+['"]|\d+|[\w.\[\]]+)(?:\s*(==|!=)\s*(['"][^'"]+['"]|\d+|[\w.]+))?\s*/);
            if (lastResult === false || !split) {
                // don't bother evaluating anymore if lastResult is already false
                // or if split failed
                return false;
            } else {
                var operand1 = typeCheck ?
                    Object.prototype.toString.call(this._parseValue(json, split[1])) :
                    this._parseValue(json, split[1]);
                var op = split[2] ? split[2] : 'unary';
                var operand2 = split[3] ? this._parseValue(json, split[3]) : null;

                var thisResult = this._evaluateOperation(op, operand1, operand2);

                if (lastResult == undefined) {
                    lastResult = thisResult;
                } else if (lastResult) {
                    lastResult = lastResult && thisResult;
                } else {
                    // if lastResult is already false, 
                    return lastResult;
                }
            }
        }

        return lastResult;
    },

    /**
    * Process the Object Property field string through the entity object to find the data requested
    *
    * @param {Obj} obj
    * @param {String} properties
    * @returns {String} Ultimatelly will return a string with the data
    */
    _readEntity: function(obj, properties) {
        if(!obj || obj === undefined) {
            return false;
        }

        var steps = properties.split('.');
        var currStep = steps[0];
        steps.shift();

        var isArrPath = currStep.match(/^(\S+)\[(\d+|\*)\]$/);
        var arrKey = isArrPath ? isArrPath[1] : currStep + '';
        var arrIndex = isArrPath ? parseInt(isArrPath[2]) : null;

        if(isArrPath && obj[arrKey] != undefined) {
            return steps.length > 0 ?
                this._readEntity(obj[arrKey][arrIndex], steps.join('.')) : 
                obj[arrKey][arrIndex];
        }
        else if(steps.length > 0) {
            return this._readEntity(obj[currStep], steps.join('.'));
        }
        
        return obj[currStep];
    },

    /**
    * Insert or update a record based on the data gathered by the mappings
    *
    * @param {Obj} recordData
    * @param {String} dataOperation
    * @returns {void}
    */
    _actionOnRecord: function(recordData, dataOperation) {
        var objData = recordData.data;
        var objCoalesce = recordData.coalesceFields;
        var record = new GlideRecord(recordData.table);
        var doUpdate = false;

        if(Object.keys(objCoalesce).length > 0) {
            for(var field in objCoalesce) {
                record.addQuery(field, objCoalesce[field]);
            }

            record.query();
            if(!record.hasNext() && this._isUpdate(dataOperation)) return;
            doUpdate = record.next() && (this._isInsert(dataOperation) || this._isUpdate(dataOperation));
        }
        else if(this._isInsert(dataOperation)) {
            record.initialize();
        }
        else {
            // if it is an unknown data operation, do nothing
            return;
        }

        for(var data in objData) {
            record.setValue(data, objData[data]);
        }

        record.setValue('building', this.facilities['building']);
        record.setValue('floor', this.facilities['floor']);

        if(doUpdate) {
            if(!record.update()) return;
        }
        else if(this._isInsert(dataOperation)) {
            if(!record.insert()) return;
        }
        else {
            // if it is an unknown data operation, do nothing
            return;
        }
    },

    /**
    * Split string by newlines and strip needless whitespace and empty lines
    * in a single line.
    *
    * @param {String} string
    * @returns {String}
    */
    _stripWhitespace: function(string) {
        return string
            .split(/[\n\r]+/g)
            .map(function(s){return s.trim();})
            // remove empty lines
            .filter(function(s){return s !== '';})
            .join(' ');
    },

    /**
    * Parse a value from string, number, boolean or a JSON path
    *
    * To parse directly to a string it needs to be surrounded by quotes.
    *
    * @param {Obj} json
    * @param {String} str
    * @returns {String/Number/Boolean}
    */
    _parseValue: function(json, str) {
        var strLower = str.toLowerCase();
        if (this._isString(str)) {
            return str.substr(1, str.length-2);
        } else if (!isNaN(parseInt(str))) {
            return parseInt(str);
        } else if (strLower == 'true' || strLower == 'false') {
            return str == 'true';
        }
        return this._readEntity(json, str);
    },

    /**
    * Check if it's a string surrounded by quotes
    *
    * @param {String} string
    * @returns {Boolean}
    */
    _isString: function(string) {
        if (!string || typeof string !== 'string')
            return false;
        string = string.trim();
        // disallow empty strings and ' " '' "" strings
        return string.length >= 3 &&
            string.match(/^(['"])[^\1]+\1$/);
    },

    /**
    * Operation evaluator
    *
    * @param {string} op (only ==/!=/'unary' supported for now)
    * @param {object} operand1
    * @param {object} operand2
    * @returns {boolean} returns result of used operation
    */
    _evaluateOperation: function(op, operand1, operand2) {
        var thisResult;
        switch (op) {
            case '==':
                thisResult = operand1 == operand2;
                break;
            case '!=':
                thisResult = operand1 != operand2;
                break;
            case 'unary':
                thisResult = (typeof operand1 == 'number') ? true : !(!operand1);
                break;
            default:
                thisResult = false;
        }
        return thisResult;
    },

    /**
    * Function to run the transform script
    * 
    * @param {Object} objMappingRec
    * @param {Object} entity
    * @returns {String} fieldValue
    */
    _runTransformScript: function(objMappingRec, entity) {
        var handler = "try{" + objMappingRec.transform_script + "}catch(err){gs.info('There was an error executing the Transform Script: '+err);}";
        objMappingRec.transform_script = handler;
        var evaluate = new GlideScopedEvaluator();
        evaluate.putVariable('entity', entity);
        var fieldValue = evaluate.evaluateScript(objMappingRec, 'transform_script', null);

        return fieldValue;
    },

    _isConnect: function(operation) {
        return operation === 'connect';
    },

    _isUpdate: function(operation) {
        return operation === 'update';
    },

    _isInsert: function(operation) {
        return operation === 'insert';
    },

    type: 'FMFPdxfMapper'
};